/**
 * Copyright (c) 2008-2012, JGraph Ltd
 */
package com.mxgraph.swing.handler;

import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.swing.util.mxSwingConstants;
import com.mxgraph.swing.view.mxCellStatePreview;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.LinkedList;

/**
 * Connection handler creates new connections between cells. This control is used to display the connector
 * icon, while the preview is used to draw the line.
 */
public class mxMovePreview extends mxEventSource {
    /**
     * The Graph component.
     */
    protected mxGraphComponent graphComponent;

    /**
     * Maximum number of cells to preview individually. Default is 200.
     */
    protected int threshold = 200;

    /**
     * Specifies if the placeholder rectangle should be used for all
     * previews. Default is false. This overrides all other preview
     * settings if true.
     */
    protected boolean placeholderPreview = false;

    /**
     * Specifies if the preview should use clones of the original shapes.
     * Default is true.
     */
    protected boolean clonePreview = true;

    /**
     * Specifies if connected, unselected edges should be included in the
     * preview. Default is true. This should not be used if cloning is
     * enabled.
     */
    protected boolean contextPreview = true;

    /**
     * Specifies if the selection cells handler should be hidden while the
     * preview is visible. Default is false.
     */
    protected boolean hideSelectionHandler = false;

    /**
     * The Start state.
     */
    protected transient mxCellState startState;

    /**
     * The Preview states.
     */
    protected transient mxCellState[] previewStates;

    /**
     * The Moving cells.
     */
    protected transient Object[] movingCells;

    /**
     * The Initial placeholder.
     */
    protected transient Rectangle initialPlaceholder;

    /**
     * The Placeholder.
     */
    protected transient Rectangle placeholder;

    /**
     * The Last dirty.
     */
    protected transient mxRectangle lastDirty;

    /**
     * The Preview.
     */
    protected transient mxCellStatePreview preview;

    /**
     * Constructs a new rubberband selection for the given graph component.
     *
     * @param graphComponent Component that contains the rubberband.
     */
    public mxMovePreview(mxGraphComponent graphComponent) {
        this.graphComponent = graphComponent;

        // Installs the paint handler
        graphComponent.addListener(mxEvent.AFTER_PAINT, new mxIEventListener() {
            public void invoke(Object sender, mxEventObject evt) {
                Graphics g = (Graphics) evt.getProperty("g");
                paint(g);
            }
        });
    }

    /**
     * Gets threshold.
     *
     * @return the threshold
     */
    public int getThreshold() {
        return threshold;
    }

    /**
     * Sets threshold.
     *
     * @param value the value
     */
    public void setThreshold(int value) {
        threshold = value;
    }

    /**
     * Is placeholder preview boolean.
     *
     * @return the boolean
     */
    public boolean isPlaceholderPreview() {
        return placeholderPreview;
    }

    /**
     * Sets placeholder preview.
     *
     * @param value the value
     */
    public void setPlaceholderPreview(boolean value) {
        placeholderPreview = value;
    }

    /**
     * Is clone preview boolean.
     *
     * @return the boolean
     */
    public boolean isClonePreview() {
        return clonePreview;
    }

    /**
     * Sets clone preview.
     *
     * @param value the value
     */
    public void setClonePreview(boolean value) {
        clonePreview = value;
    }

    /**
     * Is context preview boolean.
     *
     * @return the boolean
     */
    public boolean isContextPreview() {
        return contextPreview;
    }

    /**
     * Sets context preview.
     *
     * @param value the value
     */
    public void setContextPreview(boolean value) {
        contextPreview = value;
    }

    /**
     * Is hide selection handler boolean.
     *
     * @return the boolean
     */
    public boolean isHideSelectionHandler() {
        return hideSelectionHandler;
    }

    /**
     * Sets hide selection handler.
     *
     * @param value the value
     */
    public void setHideSelectionHandler(boolean value) {
        hideSelectionHandler = value;
    }

    /**
     * Is active boolean.
     *
     * @return the boolean
     */
    public boolean isActive() {
        return startState != null;
    }

    /**
     * FIXME: Cells should be assigned outside of getPreviewStates
     *
     * @return the object [ ]
     */
    public Object[] getMovingCells() {
        return movingCells;
    }

    /**
     * Get cells object [ ].
     *
     * @param initialState the initial state
     * @return the object [ ]
     */
    public Object[] getCells(mxCellState initialState) {
        mxGraph graph = graphComponent.getGraph();

        return graph.getMovableCells(graph.getSelectionCells());
    }

    /**
     * Returns the states that are affected by the move operation.
     *
     * @return the mx cell state [ ]
     */
    protected mxCellState[] getPreviewStates() {
        mxGraph graph = graphComponent.getGraph();
        Collection<mxCellState> result = new LinkedList<mxCellState>();

        for (Object cell : movingCells) {
            mxCellState cellState = graph.getView().getState(cell);

            if (cellState != null) {
                result.add(cellState);

                // Terminates early if too many cells
                if (result.size() >= threshold) {
                    return null;
                }

                if (isContextPreview()) {
                    Object[] edges = graph.getAllEdges(new Object[]{cell});

                    for (Object edge : edges) {
                        if (!graph.isCellSelected(edge)) {
                            mxCellState edgeState = graph.getView().getState(
                                    edge);

                            if (edgeState != null) {
                                // Terminates early if too many cells
                                if (result.size() >= threshold) {
                                    return null;
                                }

                                result.add(edgeState);
                            }
                        }
                    }
                }
            }
        }

        return result.toArray(new mxCellState[result.size()]);
    }

    /**
     * Is cell opaque boolean.
     *
     * @param cell the cell
     * @return the boolean
     */
    protected boolean isCellOpaque(Object cell) {
        return startState != null && startState.getCell() == cell;
    }

    /**
     * Sets the translation of the preview.
     *
     * @param e     the e
     * @param state the state
     */
    public void start(MouseEvent e, mxCellState state) {
        startState = state;
        movingCells = getCells(state);
        previewStates = (!placeholderPreview) ? getPreviewStates() : null;

        if (previewStates == null || previewStates.length >= threshold) {
            placeholder = getPlaceholderBounds(startState).getRectangle();
            initialPlaceholder = new Rectangle(placeholder);
            graphComponent.getGraphControl().repaint(placeholder);
        }

        fireEvent(new mxEventObject(mxEvent.START, "event", e, "state",
                startState));
    }

    /**
     * Gets placeholder bounds.
     *
     * @param startState the start state
     * @return the placeholder bounds
     */
    protected mxRectangle getPlaceholderBounds(mxCellState startState) {
        mxGraph graph = graphComponent.getGraph();

        return graph.getView().getBounds(graph.getSelectionCells());
    }

    /**
     * Create cell state preview mx cell state preview.
     *
     * @return the mx cell state preview
     */
    public mxCellStatePreview createCellStatePreview() {
        return new mxCellStatePreview(graphComponent, isClonePreview()) {
            protected float getOpacityForCell(Object cell) {
                if (isCellOpaque(cell)) {
                    return 1;
                }

                return super.getOpacityForCell(cell);
            }
        };
    }

    /**
     * Sets the translation of the preview.
     *
     * @param e     the e
     * @param dx    the dx
     * @param dy    the dy
     * @param clone the clone
     */
    public void update(MouseEvent e, double dx, double dy, boolean clone) {
        mxGraph graph = graphComponent.getGraph();

        if (placeholder != null) {
            Rectangle tmp = new Rectangle(placeholder);
            placeholder.x = initialPlaceholder.x + (int) dx;
            placeholder.y = initialPlaceholder.x + (int) dy;
            tmp.add(placeholder);
            graphComponent.getGraphControl().repaint(tmp);
        } else if (previewStates != null) {
            preview = createCellStatePreview();
            preview.setOpacity(graphComponent.getPreviewAlpha());

            // Combines the layout result with the move preview
            for (mxCellState previewState : previewStates) {
                preview.moveState(previewState, dx, dy, false, false);

                // FIXME: Move into show-handler?
                boolean visible = true;

                if ((dx != 0 || dy != 0) && clone && isContextPreview()) {
                    visible = false;
                    Object tmp = previewState.getCell();

                    while (!visible && tmp != null) {
                        visible = graph.isCellSelected(tmp);
                        tmp = graph.getModel().getParent(tmp);
                    }
                }
            }

            mxRectangle dirty = lastDirty;

            lastDirty = preview.show();

            if (dirty != null) {
                dirty.add(lastDirty);
            } else {
                dirty = lastDirty;
            }

            if (dirty != null) {
                repaint(dirty);
            }
        }

        if (isHideSelectionHandler()) {
            graphComponent.getSelectionCellsHandler().setVisible(false);
        }

        fireEvent(new mxEventObject(mxEvent.CONTINUE, "event", e, "dx", dx,
                "dy", dy));
    }

    /**
     * Repaint.
     *
     * @param dirty the dirty
     */
    protected void repaint(mxRectangle dirty) {
        if (dirty != null) {
            graphComponent.getGraphControl().repaint(dirty.getRectangle());
        } else {
            graphComponent.getGraphControl().repaint();
        }
    }

    /**
     * Reset.
     */
    protected void reset() {
        mxGraph graph = graphComponent.getGraph();

        if (placeholder != null) {
            Rectangle tmp = placeholder;
            placeholder = null;
            graphComponent.getGraphControl().repaint(tmp);
        }

        if (isHideSelectionHandler()) {
            graphComponent.getSelectionCellsHandler().setVisible(true);
        }

        // Revalidates the screen
        // TODO: Should only revalidate moved cells
        if (!isClonePreview() && previewStates != null) {
            graph.getView().revalidate();
        }

        previewStates = null;
        movingCells = null;
        startState = null;
        preview = null;

        if (lastDirty != null) {
            graphComponent.getGraphControl().repaint(lastDirty.getRectangle());
            lastDirty = null;
        }
    }

    /**
     * Stop object [ ].
     *
     * @param commit the commit
     * @param e      the e
     * @param dx     the dx
     * @param dy     the dy
     * @param clone  the clone
     * @param target the target
     * @return the object [ ]
     */
    public Object[] stop(boolean commit, MouseEvent e, double dx, double dy,
                         boolean clone, Object target) {
        Object[] cells = movingCells;
        reset();

        mxGraph graph = graphComponent.getGraph();
        graph.getModel().beginUpdate();
        try {
            if (commit) {
                double s = graph.getView().getScale();
                cells = graph.moveCells(cells, dx / s, dy / s, clone, target,
                        e.getPoint());
            }

            fireEvent(new mxEventObject(mxEvent.STOP, "event", e, "commit",
                    commit));
        } finally {
            graph.getModel().endUpdate();
        }

        return cells;
    }

    /**
     * Paint.
     *
     * @param g the g
     */
    public void paint(Graphics g) {
        if (placeholder != null) {
            mxSwingConstants.PREVIEW_BORDER.paintBorder(graphComponent, g,
                    placeholder.x, placeholder.y, placeholder.width,
                    placeholder.height);
        }

        if (preview != null) {
            preview.paint(g);
        }
    }

}
